/** Recursive Object Notation for JavaScript.
 *
 * RON is a method of converting an object graph of JavaScript objects which
 * contain circular references to a JSON representation and back again.
 */
var ron =
{
	/** The constructor table. */
	_ctors: {},
	/** Register a constructor function with the RON system.
	 *@param ctor The constructor.
	 *@param id A unique ID for the constructor.
	 */
	registerCtor: function(ctor, id)
	{
		if(this._ctors[id] !== undefined)
			throw "Duplicate constructor ID \"" + id + "\" registered.";
		this._ctors[id] = ctor;
		ctor.prototype.__ = id;
	},
	/** Serialize an object graph.
	 *@param obj The object graph.
	 *@param version The version of the object graph. Set this to null to opt-out.
	 */
	serialize: function(obj, version)
	{
		var i, ret, out =
		{
			version: version,
			objects: [],
			sourceObjects: []
		};
		out.root = this._serializeObject(obj, out);

		// Clean up after ourselves
		for(i = 0; i < out.sourceObjects.length; ++i)
		{
			out.sourceObjects[i]._ = undefined;
			/*[targets=html]*/delete(out.sourceObjects[i]._);/*[/targets]*/
		}
		out.sourceObjects = undefined;
		/*[targets=html]*/delete(out.sourceObjects);/*[/targets]*/
		
		ret = JSON.stringify(out);
		return ret;
	},
	/** Deserialize an object graph.
	 *@param data The serialized data.
	 *@param version The version of the object graph expected. Set this to null to accept any version.
	 *@returns The object graph, or null if there was an error. The ron.error string describes the error.
	 */
	deserialize: function(data, version)
	{
		var i, out = JSON.parse(data);

		if(version !== null &&
			version !== undefined &&
			out.version !== version)
			throw "Version mismatch: expected version " + version + " but the retrieved data was for version " + out.version;

		// First we deserialize all objects in the object table into a pool of
		// new objects.
		out.newObjects = [];
		for(i = 0; i < out.objects.length; ++i)
		{
			out.newObjects.push(this._deserializeObject(out.objects[i]));
		}

		// Now we link all of the object references from the object table into
		// the new objects. We do it this way to avoid processing objects that
		// were not present in the serialized data form.
		for(i = 0; i < out.objects.length; ++i)
		{
			this._relinkObject(out.objects[i], out.newObjects[i], out);
		}
		out.root = out.newObjects[out.root._];

		// Finally call the fromRON method for all our new objects
		for(i = 0; i < out.newObjects.length; ++i)
		{
			if(out.newObjects[i].fromRON)
				out.newObjects[i].fromRON();
		}
		
		return out.root;
	},
	/** Re-link any object references an object might have.
	 *@param serializedObject The serialized object to relink.
	 *@param newObject The new object we are linking to.
	 *@param out The serialization dataset.
	 */
	_relinkObject: function(serializedObject, newObject, out)
	{
		var o;

		// Object does not need relinking
		if(serializedObject === undefined ||
			serializedObject === null)
			return;

		// Look for child objects to link
		for(o in serializedObject)
		{
			if(serializedObject[o] !== null &&
				typeof(serializedObject[o]) === "object" &&
				serializedObject[o]._ != undefined)
			{
				// We have an object reference in the serialized object, so
				// create a pointer in the new object to the new object that
				// we are referencing.
				newObject[o] = out.newObjects[serializedObject[o]._];
			}
		}
	},
	/** Deserialize a single object in an object graph.
	 *@param obj The object being deserialized.
	 */
	_deserializeObject: function(obj)
	{
		var n, o;

		// Object needs no special processing.
		if(obj === undefined ||
			obj === null)
			return obj;

		// Special handling for arrays
		if(obj instanceof Array)
		{
			return obj.slice();
		}
		
		// Reconstruct the object from its constructor ID if it has one.
		if(obj.__ !== undefined)
		{
			var ctor = this._ctors[obj.__];
			if(ctor === undefined)
				throw "Unregistered constructor \"" + obj.__ + "\" referenced in serialized data stream.";
			n = new ctor();
		}
		// Otherwise just create a new empty object.
		else
		{
			n = {};
		}

		// Copy everything from the serialized object to the new object (except
		// the constructor ID).
		for(o in obj)
		{
			if(o != "__")
				n[o] = obj[o];
		}
		return n;
	},
	/** Serialize a single object in the object graph.
	 *@param obj The object to process.
	 *@param out The output dataset.
	 */
	_serializeObject: function(obj, out)
	{
		var t, n, o, i;

		// Null and undefined
		if(obj === null ||
			obj === undefined)
			return obj;

		// Object has already been serialized, return the placeholder
		if(obj._ !== undefined)
			return {_:obj._};

		// Special handling for arrays
		if(obj instanceof Array)
		{
			// Add the object to the object pool
			obj._ = out.objects.length;
			out.sourceObjects.push(obj);
			n = [];
			out.objects.push(n);

			for(i = 0; i < obj.length; ++i)
			{
				switch(typeof(obj[i]))
				{
					case "object":
						n.push(this._serializeObject(obj[i], out));
						break;
					case "boolean":
					case "number":
					case "string":
						n.push(obj[i]);
						break;
					default:
						n.push(null);
						break;
				}
			}
		}
		// Normal objects
		else
		{
			// If the object defines it's own toRON function use that to get the
			// serialized form.
			t = obj;
			if(obj.toRON)
				t = obj.toRON();
			// Do a member-wise copy of the serialized form.
			n = {};
			for(o in t)
			{
				if(!t.hasOwnProperty(o))
					continue;
				switch(typeof(t[o]))
				{
					case "object":
						// Handle arrays specially
						if(t[o] instanceof Array)
							n[o] = t[o].slice();
						else
							n[o] = t[o];
					case "boolean":
					case "number":
					case "string":
						n[o] = t[o];
						break;
				}
			}

			// Add the object to the object pool
			obj._ = out.objects.length;
			out.sourceObjects.push(obj);
			out.objects.push(n);

			// Process child objects
			for(o in n)
			{
				if(!n.hasOwnProperty(o))
					continue;
				if(typeof(n[o]) === "object")
					n[o] = this._serializeObject(n[o], out);
			}

			// Add the ctor ID to the object if present
			if(obj.__ !== undefined)
			{
				n.__ = obj.__;
			}
		}

		// Return the placeholder for this object
		return {_:obj._};
	}
};
